/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2009, The KiWi Project (http://www.kiwi-project.eu)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * Szaby Gruenwald
 *
 */

package kiwi.core.services.config;

import kiwi.core.api.config.ConfigurationService;
import kiwi.core.events.ConfigurationChangedEvent;
import kiwi.core.model.user.KiWiUser;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Any;
import javax.inject.Inject;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * @author Sebastian Schaffert
 *
 */
@ApplicationScoped
public class ConfigurationServiceImpl implements ConfigurationService {


    @Inject
    private Logger log;

  	
 	private String configFile;
 	
 	private Configuration config;
 	private HashMap<String,Configuration> userConfigurations;



    @Inject @Any
    private Event<ConfigurationChangedEvent> configurationEvent;


    /**
     * Transforms a given path from relative path to an absolute
     * path. More precisely if the given <code>path</code>
     * argument does not start with / then the given path is
     * considered relative path and it is prefixed with the
     * current running directory.<br>
     * If the path starts with / then the same string is
     * returned.
     * 
     * @param path the path to analyze.
     * @return a absolute path, if the given <code>path</code>
     *         argument starts with / character or the same
     *         String if it starts.
     */
    private String getPath(String path) {

        if (new File(path).isAbsolute()) {
            return path;
        }

        final String prefix = System.getProperty("user.dir");
        final StringBuffer result = new StringBuffer(prefix);
        result.append(File.separator);
        result.append(path);

        return result.toString();
    }
    
    public ConfigurationServiceImpl() {

        userConfigurations = new HashMap<String, Configuration>();

    }


    @PostConstruct
	public void initialize() {
		log.info("KiWi ConfigurationService starting up ...");

		Configuration bootstrapProperties = initBootstrapConfiguration();

        // setup KiWi home - if it is given as system property, the bootstrap configuration is overwritten
        if(System.getProperty("kiwi.home") != null) {
            bootstrapProperties.setProperty("kiwi.home",System.getProperty("kiwi.home"));
            bootstrapProperties.setProperty("solr.home",System.getProperty("kiwi.home")+File.separator+"solr");
            bootstrapProperties.setProperty("sesame.home",System.getProperty("kiwi.home")+File.separator+"triples");
        }


        // testing mode: remove already existing kiwi home directory
        if("true".equals(System.getProperty("kiwi.testing"))) {
            File f = new File(bootstrapProperties.getString("kiwi.home"));
            if(f.exists()) {
                log.info("TESTING: deleting existing KiWi home directory ... ");
                try {
                    FileUtils.deleteDirectory(f);
                } catch (IOException e) {
                    log.error("could not delete existing KiWi home directory",e);
                }
            }
        }


        if(bootstrapProperties.getProperty("kiwi.home") != null) {
            File f = new File(bootstrapProperties.getString("kiwi.home"));
            if(!f.exists()) {
                f.mkdirs();
            }
        }

     	// initialise Apache Commons Configuration using a XML-file-based configuration

		try {
            if(bootstrapProperties.getString("kiwi.home") != null) {
                configFile = bootstrapProperties.getString("kiwi.home") + File.separator + "system-config.properties";
                File f = new File(configFile);
                if(!f.exists()) {
                    f.createNewFile();
                }
                config = new PropertiesConfiguration(f);
            } else {
                log.error("error while initialising configuration: no kiwi.home property given; creating memory-only configuration");
                config = new MapConfiguration(new HashMap());

            }
		} catch (Exception e) {
			log.error("error while initialising configuration file {}: {}; creating memory-only configuration",configFile, e.getMessage());
			config = new MapConfiguration(new HashMap());
		}
		
		// copy over non-existing keys of bootstrap configuration
		for(Iterator<String> it = bootstrapProperties.getKeys(); it.hasNext(); ) {
            String key = it.next();

            if(!config.containsKey(key)) {
			    config.setProperty(key, bootstrapProperties.getProperty(key));
            }
		}
		

		if(config.getString("kiwi.home") != null) {
			File f = new File(bootstrapProperties.getString("kiwi.home") + File.separator + "config");
			f.mkdirs();

            File f2 = new File(bootstrapProperties.getString("kiwi.home") + File.separator + "plugins");
            f2.mkdirs();
		}
		

        save();

        initPluginConfiguration();
        initSolrConfiguration();
	}
	

    private Configuration initBootstrapConfiguration() {
        log.info("KiWi Configuration Service: looking up bootstrap configuration...");

        try {
            if("true".equals(System.getProperty("kiwi.testing"))) {
                // in testing mode we return a special configuration that has different settings for testing
                Configuration bootstrapConfig = new PropertiesConfiguration("test-config.properties");

                return bootstrapConfig;

            } else {
                Configuration bootstrapConfig = new PropertiesConfiguration("default-config.properties");

                return bootstrapConfig;
            }
        } catch (ConfigurationException e) {
            log.error("Error while trying to load the default configuration {}",e.getMessage());

            Configuration bootstrapConfig = new MapConfiguration(new HashMap());

            // FIXME: do we really want that to be hard-coded?
            bootstrapConfig.setProperty("kiwi.home","/tmp/kiwi");

            return bootstrapConfig;

        }


    }


    private void initPluginConfiguration() {
        // check whether system property is set and copy all plugins to the kiwi plugin directory

        if(System.getProperty("kiwi.plugins.deploy") != null) {
            File pluginsDeploy = new File(System.getProperty("kiwi.plugins.deploy"));
            if(pluginsDeploy.exists()) {
                log.info("deploying plugins from {} based on system property kiwi.plugins.deploy",pluginsDeploy.getAbsolutePath());

                File pluginsDir = new File(config.getString("kiwi.home") + File.separator + "plugins");

                try {
                    FileUtils.copyDirectory(pluginsDeploy,pluginsDir,new FileFilter() {
                        @Override
                        public boolean accept(File file) {
                            if(file.getName().endsWith(".jar")) {
                                log.info("deploying plugin {}",file.getName());
                                return true;
                            } else {
                                return false;
                            }
                        }
                    });
                } catch (IOException e) {
                    log.error("error while copying plugins from deploy directory to plugin directory",e);
                }
            }
        }

    }

    private void initSolrConfiguration() {

        // set the SOLR home via the appropriate system property; we have to ensure that SOLR is actually loaded
        // AFTER the configuration is started

        if (config.containsKey("solr.home")) {
            // in this way the solr can use relative and absolute
            // path.
            final String solrHomeConf = getStringConfiguration("solr.home");
            final String realPath = getPath(solrHomeConf);
            System.setProperty("solr.solr.home", realPath);
        } else {
            System.setProperty("solr.solr.home", getWorkDir() + "/solr");
        }

        System.setProperty("solr.data.dir",System.getProperty("solr.solr.home")+"/data");


        // check whether the directory already exists and which configuration version it is

        try {
            String solrHome = System.getProperty("solr.solr.home");
            File f_solrHome = new File(solrHome);
            if(f_solrHome.exists() && f_solrHome.isDirectory()) {
                // check readability and version
                if(! (f_solrHome.canRead() && f_solrHome.canWrite())) {
                    log.warn("SOLR home is not readable/writeable; please check the permissions in the file system");
                }
                File f_version = new File(f_solrHome,"VERSION");

                if(f_version.exists() && f_version.canRead()) {
                    String version = FileUtils.readFileToString(f_version);

                    if(!version.equals(getStringConfiguration("kiwi.version"))) {
                        // update required
                        unpackSolrHome(f_solrHome);
                    }

                    log.info("SOLR home directory exists and has the right version; no update required");
                } else {
                    // no version file, update required
                    unpackSolrHome(f_solrHome);
                }

            } else {
                // SOLR home does not exist, so we create it
                if(f_solrHome.mkdirs()) {
                    unpackSolrHome(f_solrHome);
                } else {
                    log.error("could not create SOLR home directory; SOLR will not work properly");
                }
            }
        } catch(IOException ex) {
            log.error("error while trying to setup SOLR home directory: {}", ex.getMessage());
        }

    }

    /**
     * This method takes as argument a File representing the SOLR home directory and unpacks the kiwi-solr-data zip
     * file that is contained in the kiwi-core.jar file into this directory. It also creates the version information
     * file to determine the KiWi version used for setting up the KiWi home directory.
     *
     * TODO: the method should afterwards trigger a reindexing of the SOLR index
     *
     * @param directory
     * @throws IOException
     */
    private void unpackSolrHome(File directory) throws IOException {
        String version = getStringConfiguration("kiwi.version");

        URL url_zip = ConfigurationServiceImpl.class.getResource("/kiwi-solr-data-"+version+".zip");

        File tmp_zip = File.createTempFile("kiwi-solr-data",".zip");

        FileUtils.copyURLToFile(url_zip,tmp_zip);

        ZipFile zipFile = new ZipFile(tmp_zip);

        Enumeration entries = zipFile.entries();

        while(entries.hasMoreElements()) {
            ZipEntry entry = (ZipEntry)entries.nextElement();

            if(entry.isDirectory()) {
                log.info("creating SOLR directory: {}", entry.getName());
                // This is not robust, just for demonstration purposes.

                File dir = new File(directory,entry.getName());

                dir.mkdirs();

                continue;
            }

            log.info("extracting SOLR configuration file: {}", entry.getName());

            File file = new File(directory,entry.getName());
            IOUtils.copy(zipFile.getInputStream(entry), new FileOutputStream(file));


        }

        zipFile.close();

        File f_version = new File(directory,"VERSION");


        FileUtils.writeStringToFile(f_version,version);
    }


    /**
     * Get the base URI out of the current request. The base URI
     * is used e.g. to generate URIs of internal content items
     * 
     * @see kiwi.core.api.config.ConfigurationService#getBaseUri()
     */
	
	public String getBaseUri() {
        return getStringConfiguration("kiwi.context");
	}


    /**
     * Get the server uri of the system, i.e. a uri that when entered in the browser accesses the
     * server that runs the KiWi (and SOLR) applications. Can be used to compute the paths of
     * other applications relative to the current application. Computed like the base uri.
     *
     * @return
     */
    @Override
    public String getServerUri() {
        return getStringConfiguration("kiwi.host");
    }


    /**
     * List all configuration keys defined for the system configuration of KiWi.
     *
     * @return
     */
    @Override
    public List<String> listConfigurationKeys() {
        LinkedList<String> keys = new LinkedList<String>();
        for(Iterator<String> it = config.getKeys(); it.hasNext(); ) {
            keys.add(it.next());
        }
        return keys;
    }

    /* (non-Javadoc)
    * @see kiwi.api.config.ConfigurationService#isConfigurationSet(java.lang.String)
    */
	@Override
	public boolean isConfigurationSet(String key) {
		return config.containsKey(key);
	}


    /**
     * Get the configuration for the given key. If there is no such configuration, a new one is
     * created with empty value (returns null).
     *
     * @param key unique configuration key for lookup
     * @return a configuration object with either the configured value or null as value
     */
    @Override
    public Object getConfiguration(String key) {
        return config.getProperty(key);
    }


    /**
     * Set the configuration "key" to the string value "value".
     *
     * @param key
     * @param value
     */
    @Override
    public void setConfiguration(String key, Object value) {
        config.setProperty(key,value);
        save();

        configurationEvent.fire(new ConfigurationChangedEvent(key));
    }

    /**
	 *
	 *
	 */
	@Override
	public String getStringConfiguration(String key) {
		return config.getString(key);
	}

	@Override
	public String getStringConfiguration(String key, String defaultValue) {
		return config.getString(key, defaultValue);
	}
	
	@Override
	public double getDoubleConfiguration(String key) {
		return config.getDouble(key, 0.0);
    }
	
	@Override
	public double getDoubleConfiguration(String key, double defaultValue) {
		return config.getDouble(key, defaultValue);
    }
	
	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#setDoubleConfiguration(java.lang.String, double)
	 */
	@Override
	public void setDoubleConfiguration(String key, double value) {
		config.setProperty(key, value);
		save();

        configurationEvent.fire(new ConfigurationChangedEvent(key));
	}

	@Override
	public int getIntConfiguration(String key) {
		return config.getInt(key, 0);
    }
	
	@Override
	public int getIntConfiguration(String key, int defaultValue) {
		return config.getInt(key, defaultValue);
    }
	
	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#setIntConfiguration(java.lang.String, int)
	 */
	@Override
	public void setIntConfiguration(String key, int value) {
		config.setProperty(key, value);
		save();
        configurationEvent.fire(new ConfigurationChangedEvent(key));
	}
	
	@Override
	public boolean getBooleanConfiguration(String key) {
		return config.getBoolean(key, true);
    }
	
	@Override
	public boolean getBooleanConfiguration(String key, boolean defaultValue) {
		return config.getBoolean(key, defaultValue);
    }
	
	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#setIntConfiguration(java.lang.String, int)
	 */
	@Override
	public void setBooleanConfiguration(String key, boolean value) {
		config.setProperty(key, value);
		save();
        configurationEvent.fire(new ConfigurationChangedEvent(key));
	}

	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#getListConfiguration(java.lang.String)
	 */
	@Override
    
	public List<String> getListConfiguration(String key) {
		return config.getList(key,Collections.EMPTY_LIST);
	}

	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#getListConfiguration(java.lang.String, java.util.List)
	 */
	@Override
    
	public List<String> getListConfiguration(String key, List<String> defaultValue) {
		return config.getList(key, defaultValue);
	}

	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#setListConfiguration(java.lang.String, java.util.List)
	 */
	@Override
	public void setListConfiguration(String key, List<String> value) {
		config.setProperty(key, value);
		save();
        configurationEvent.fire(new ConfigurationChangedEvent(key));
	}

	public void removeConfiguration(String key) {
		config.clearProperty(key);
	}

	public void setConfiguration(String key, String value) {
		config.setProperty(key, value);
		save();
        configurationEvent.fire(new ConfigurationChangedEvent(key));
	}

	@Override
	public void setConfiguration(String key, List<String> values) {
		config.setProperty(key, values);
		save();
        configurationEvent.fire(new ConfigurationChangedEvent(key));
	}
	
	
	

	public Configuration getUserConfiguration(KiWiUser user) {
		Configuration userConfig = userConfigurations.get(user.getLogin());
		if(userConfig == null) {
 		
			String userConfigFile = getConfiguration("kiwi.work.dir") + File.separator + "config" + File.separator + user.getLogin() + ".conf.xml"; 
	
			try {
				File f = new File(userConfigFile);
				if(f.exists()) {
					f.createNewFile();
				}
				userConfig = new PropertiesConfiguration(f);
			} catch(Exception ex) {
				log.error("could not create user configuration in file #0: #1",userConfigFile,ex.getMessage());
				userConfig = new MapConfiguration(new HashMap());
			}
			userConfigurations.put(user.getLogin(),userConfig);
		}
		return userConfig;
	}
	
	

	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#isUserConfigurationSet(kiwi.model.user.KiWiUser, java.lang.String)
	 */
	@Override
	public boolean isUserConfigurationSet(KiWiUser user, String key) {
		return getUserConfiguration(user).containsKey(key);
	}

	@Override
	public String getUserConfiguration(KiWiUser user, String key, String defaultValue) {
		return getUserConfiguration(user).getString(key, defaultValue);
	}
	

	@Override
	public String getUserConfiguration(KiWiUser user, String key) {
		return getUserConfiguration(user).getString(key);
	}
	

	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#getUserListConfiguration(kiwi.model.user.KiWiUser, java.lang.String)
	 */
	@Override
	public List getUserListConfiguration(KiWiUser user, String key) {
		return getUserListConfiguration(user, key, Collections.EMPTY_LIST);
	}

	/* (non-Javadoc)
	 * @see kiwi.api.config.ConfigurationService#getUserListConfiguration(kiwi.model.user.KiWiUser, java.lang.String, java.util.List)
	 */
	@Override
	public List getUserListConfiguration(KiWiUser user, String key, List defaultValue) {
		return getUserConfiguration(user).getList(key, defaultValue);
	}

	@Override
	public void removeUserConfiguration(KiWiUser user, String key) {
		getUserConfiguration(user).clearProperty(key);
	}

	@Override
	public void setUserListConfiguration(KiWiUser user, String key, List<String> values) {
		getUserConfiguration(user).setProperty(key, values);
	}

	@Override
	public void setUserConfiguration(KiWiUser user, String key, String value) {
		getUserConfiguration(user).setProperty(key, value);
	}
	

	/**
     * The work directory of the Sesame 2 native store. Sesame will create its own subdirectory
     * beneath this directory called "triples" and store the native database there.
     */
    
	public String getWorkDir() {
		final String value = getStringConfiguration("kiwi.home");
		return value!=null?value:"/tmp/kiwi";
	}


    @Override
    public String getPluginDirectory() {
        return config.getString("kiwi.home") + File.separator + "plugins";
    }


    public void save() {
    	if(config instanceof PropertiesConfiguration) {
    		try {
				((PropertiesConfiguration)config).save();
			} catch (ConfigurationException e) {
				log.error("could not save system configuration: #0",e.getMessage());
			}
    	}
    }
    
    public void save(KiWiUser user) {
    	Configuration userConfig = getUserConfiguration(user);
    	
    	if(userConfig instanceof PropertiesConfiguration) {
    		try {
				((PropertiesConfiguration)userConfig).save();
			} catch (ConfigurationException e) {
				log.error("could not save user configuration for user #0: #1",user.getLogin(),e.getMessage());
			}    		
    	}
    }
}
